package org.codefilarete.stalactite.engine.configurer.map;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.codefilarete.stalactite.mapping.ComposedIdMapping;
import org.codefilarete.stalactite.mapping.EmbeddedBeanMapping;
import org.codefilarete.stalactite.mapping.IdAccessor;
import org.codefilarete.stalactite.mapping.IdMapping;
import org.codefilarete.stalactite.mapping.id.assembly.ComposedIdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.assembly.IdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.manager.AlreadyAssignedIdentifierManager;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.stalactite.sql.result.Row;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Maps;

/**
 * {@link IdMapping} for {@link KeyValueRecord} : a composed id made of
 * - {@link KeyValueRecord#getId()}
 * - {@link KeyValueRecord#getKey()}
 */
public class KeyValueRecordIdMapping<K, I, T extends Table<T>> extends ComposedIdMapping<KeyValueRecord<K, ?, I>, RecordId<K, I>> {
	
	private KeyValueRecordIdMapping(
			RecordIdAssembler recordIdAssembler) {
		super(new KeyValueRecordIdAccessor<>(),
				new AlreadyAssignedIdentifierManager<>((Class<RecordId<K, I>>) (Class) RecordId.class,
						KeyValueRecord::markAsPersisted,
						KeyValueRecord::isPersisted),
				recordIdAssembler);
	}
	
	public <LEFTTABLE extends Table<LEFTTABLE>> KeyValueRecordIdMapping(
			T targetTable,
			Function<ColumnedRow, K> entryKeyAssembler,
			Function<K, Map<Column<T, ?>, ?>> entryKeyColumnValueProvider,
			IdentifierAssembler<I, LEFTTABLE> sourceIdentifierAssembler,
			Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping) {
		this(new RecordIdAssembler<>(targetTable, entryKeyAssembler, entryKeyColumnValueProvider, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping));
	}
	
	public <LEFTTABLE extends Table<LEFTTABLE>> KeyValueRecordIdMapping(
			T targetTable,
			EmbeddedBeanMapping<K, T> entryKeyMapping,
			IdentifierAssembler<I, LEFTTABLE> sourceIdentifierAssembler,
			Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping) {
		this(new RecordIdAssembler<>(targetTable, entryKeyMapping, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping));
	}
	
	/**
	 * Override because {@link ComposedIdMapping} is based on null identifier to determine newness, which is always false for {@link KeyValueRecord}
	 * because they always have one. We delegate its computation to the entity.
	 *
	 * @param entity any non-null entity
	 * @return true or false based on {@link KeyValueRecord#isNew()}
	 */
	@Override
	public boolean isNew(KeyValueRecord<K, ?, I> entity) {
		return entity.isNew();
	}
	
	public static class KeyValueRecordIdAccessor<K, I> implements IdAccessor<KeyValueRecord<K, ?, I>, RecordId<K, I>> {
		
		@Override
		public RecordId<K, I> getId(KeyValueRecord<K, ?, I> associationRecord) {
			return associationRecord.getId();
		}
		
		@Override
		public void setId(KeyValueRecord<K, ?, I> associationRecord, RecordId<K, I> identifier) {
			associationRecord.setId(new RecordId<>(identifier.getId(), associationRecord.getKey()));
			associationRecord.setKey(identifier.getKey());
		}
	}
	
	/**
	 * Identifier assembler for {@link RecordId}
	 *
	 * @param <KID> embedded key bean type
	 * @param <SRCID> source identifier type
	 */
	@VisibleForTesting
	static class RecordIdAssembler<KID, SRCID, T extends Table<T>> extends ComposedIdentifierAssembler<RecordId<KID, SRCID>, T> {
		
		private final Function<ColumnedRow, KID> entryKeyAssembler;
		private final Function<KID, Map<Column<T, ?>, ?>> entryKeyColumnValueProvider;
		private final IdentifierAssembler<SRCID, ?> sourceIdentifierAssembler;
		private final Map<Column<?, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping;
		
		/**
		 * Constructor mapping given values to fields
		 *
		 * @param targetTable                  association table on which value should be inserted an read
		 * @param entryKeyAssembler            builder of K instances from {@link Column} found in a {@link Row}
		 * @param entryKeyColumnValueProvider  provides {@link Column} values to be inserted / updated from a K instance
		 * @param sourceIdentifierAssembler    assemble / disassemble ID instances from / to {@link Column}s
		 * @param primaryKey2ForeignKeyMapping used to remap {@link Column}s of sourceIdentifierAssembler to association table since it gives it for LEFTTABLE
		 * @param <LEFTTABLE>                  table type of source declaring the relation
		 */
		@VisibleForTesting
		<LEFTTABLE extends Table<LEFTTABLE>> RecordIdAssembler(
				T targetTable,
				Function<ColumnedRow, KID> entryKeyAssembler,
				Function<KID, Map<Column<T, ?>, ?>> entryKeyColumnValueProvider,
				IdentifierAssembler<SRCID, LEFTTABLE> sourceIdentifierAssembler,
				Map<? extends Column<LEFTTABLE, ?>, ? extends Column<T, ?>> primaryKey2ForeignKeyMapping
		) {
			super(targetTable);
			this.entryKeyAssembler = entryKeyAssembler;
			this.entryKeyColumnValueProvider = entryKeyColumnValueProvider;
			this.sourceIdentifierAssembler = sourceIdentifierAssembler;
			this.primaryKey2ForeignKeyMapping = (Map) primaryKey2ForeignKeyMapping;
		}
		
		/**
		 * Constructor for cases where K is a simple (embedded) bean.
		 *
		 * @param targetTable                  association table on which value should be inserted an read
		 * @param entryKeyMapping              persistence mapping of K instance
		 * @param sourceIdentifierAssembler    assemble / disassemble ID instances from / to {@link Column}s
		 * @param primaryKey2ForeignKeyMapping used to remap {@link Column}s of sourceIdentifierAssembler to association table since it gives it for LEFTTABLE
		 * @param <LEFTTABLE>                  table type of source declaring the relation
		 */
		@VisibleForTesting
		<LEFTTABLE extends Table<LEFTTABLE>> RecordIdAssembler(
				T targetTable,
				EmbeddedBeanMapping<KID, T> entryKeyMapping,
				IdentifierAssembler<SRCID, LEFTTABLE> sourceIdentifierAssembler,
				Map<? extends Column<LEFTTABLE, ?>, ? extends Column<T, ?>> primaryKey2ForeignKeyMapping
		) {
			this(targetTable, entryKeyMapping::transform, entryKeyMapping::getInsertValues, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping);
		}
		
		/**
		 * Constructor for cases where entry key an entity referencing another table. Then K type becomes its identifier.
		 *
		 * @param targetTable                   association table on which value should be inserted an read
		 * @param rightTableIdentifierAssembler entity identifier mapping (K type mapping)
		 * @param sourceIdentifierAssembler     assemble / disassemble ID instances from / to {@link Column}s
		 * @param primaryKey2ForeignKeyMapping  used to remap {@link Column}s of sourceIdentifierAssembler to association table since it gives it for LEFTTABLE
		 * @param <LEFTTABLE>                   table type of source declaring the relation
		 */
		@VisibleForTesting
		<LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>> RecordIdAssembler(
				T targetTable,
				IdentifierAssembler<KID, RIGHTTABLE> rightTableIdentifierAssembler,
				IdentifierAssembler<SRCID, LEFTTABLE> sourceIdentifierAssembler,
				Map<? extends Column<LEFTTABLE, ?>, ? extends Column<T, ?>> primaryKey2ForeignKeyMapping,
				Map<Column<RIGHTTABLE, ?>, Column<T, ?>> rightTable2EntryKeyMapping
		) {
			this(targetTable, rightTableIdentifierAssembler::assemble, KID -> {
				Map<Column<RIGHTTABLE, ?>, ?> keyColumnValues = rightTableIdentifierAssembler.getColumnValues(KID);
				return Maps.innerJoin(rightTable2EntryKeyMapping, keyColumnValues);
				
			}, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping);
		}
		
		@Override
		public RecordId<KID, SRCID> assemble(ColumnedRow row) {
			SRCID SRCID = sourceIdentifierAssembler.assemble(row);
			KID entryKey = entryKeyAssembler.apply(row);
			
			return SRCID == null ? null : new RecordId<>(SRCID, entryKey);
		}
		
		@Override
		public Map<Column<T, ?>, ?> getColumnValues(RecordId<KID, SRCID> record) {
			Map<Column<?, ?>, Object> sourceColumnValues = (Map) sourceIdentifierAssembler.getColumnValues(record.getId());
			Map<Column<T, ?>, Object> idColumnValues = Maps.innerJoin(primaryKey2ForeignKeyMapping, sourceColumnValues);
			Map<Column<T, ?>, ?> entryKeyColumnValues = entryKeyColumnValueProvider.apply(record.getKey());
			Map<Column<T, ?>, Object> result = new HashMap<>();
			result.putAll(idColumnValues);
			result.putAll(entryKeyColumnValues);
			return result;
		}
	}
}
